package br.mikhas.util.json;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import br.mikhas.util.ExceptionUtils;
import br.mikhas.util.StringUtils;

/**
 * 
 * Utilities to deals with JSON serialization operations on common objects
 * 
 * @author N068815 - Mikhail Domanoski
 * 
 */
public final class JSONUtils {
	/**
	 * Make it uninstantiable
	 */
	private JSONUtils() {

	}

	/**
	 * Creates a JSON array representation of a collection of {@link JSONable}
	 * elements
	 * 
	 * @param collection
	 *            A collection of JSONable elements
	 * @return a JSON array String
	 */
	public static String toJson(Collection<?> collection) {
		StringBuilder builder = new StringBuilder();
		toJson(collection, builder);
		return builder.toString();
	}

	/**
	 * Appends a JSON array representation of a collection of {@link JSONable}
	 * elements
	 * 
	 * @param collection
	 *            A collection of JSONable elements
	 * @param builder
	 *            The build which the JSON string will be appended
	 */
	public static void toJson(Collection<?> collection, StringBuilder builder) {

		boolean first = true;

		builder.append('[');

		for (Object object : collection) {
			if (!first)
				builder.append(',');

			toJson(object, builder);

			first = false;
		}

		builder.append(']');
	}

	/**
	 * Appends a JSON object representation of a date
	 * 
	 * @param date
	 *            The date to be parsed into a JSON string
	 * @param builder
	 *            The build which the JSON string will be appended
	 */
	public static void toJson(Date date, StringBuilder builder) {
		if (!(date instanceof java.sql.Date)) {
			date = new java.sql.Date(date.getTime());
		}

		builder.append("{timestamp:").append(date.getTime());
		builder.append(",text:\"").append(date.toString());
		builder.append("\"}");
	}

	/**
	 * Creates a JSON object representation of a date
	 * 
	 * @param date
	 *            The date to be parsed into a JSON string
	 */
	public static String toJson(Date date) {
		StringBuilder builder = new StringBuilder(124);
		toJson(date, builder);
		return builder.toString();
	}

	public static void toJson(String string, StringBuilder builder) {
		builder.append('"');
		StringUtils.encodeHTML(string, builder);
		builder.append('"');
	}

	public static String toJson(String string) {
		StringBuilder builder = new StringBuilder(string.length() + 4);
		toJson(string, builder);
		return builder.toString();
	}

	/**
	 * Creates a JSON object representation of the current object
	 * 
	 * @param date
	 *            The date to be parsed into a JSON string
	 */
	public static String toJson(Object object) {
		StringBuilder builder = new StringBuilder();
		toJson(object, builder);
		return builder.toString();
	}

	/**
	 * Converts an object on it's JSON representation and appends it on the
	 * providen {@link StringBuilder}.
	 * 
	 * @param object
	 *            The object to be converted
	 * @param builder
	 *            The StringBuilder to append the converted object
	 */
	@SuppressWarnings("unchecked")
	public static void toJson(Object object, StringBuilder builder) {
		if (object == null) {
			builder.append("null");
		} else if (object instanceof JSONable) {
			JSONable jsonable = (JSONable) object;
			builder.append(jsonable.toJson());
		} else if (object instanceof String) {
			toJson(object.toString(), builder);
		} else if (object instanceof Number || object instanceof Boolean) {
			builder.append(object.toString());
		} else if (object instanceof Date) {
			toJson((Date) object, builder);
		} else if (object instanceof Map<?, ?>) {
			toJson((Map<String, Object>) object, builder);
		} else if (object instanceof Collection<?>) {
			toJson((Collection<?>) object, builder);
		} else if (object.getClass().isArray()) {
			serializeArray(object, builder);
		} else {
			ordnaryObject(object, builder);
		}
	}

	/**
	 * Creates the JSON representation of a map and appends it to the passed
	 * string builder
	 * 
	 * @param map
	 *            The map to create the representation
	 * @param builder
	 *            The StringBuilder to append the json representation
	 */
	public static void toJson(Map<String, Object> map, StringBuilder builder) {
		if (map == null) {
			builder.append("null");
			return;
		}
		boolean first = true;
		builder.append('{');
		Iterator<Entry<String, Object>> iterator = map.entrySet().iterator();

		while (iterator.hasNext()) {
			Entry<String, Object> entry = iterator.next();

			if (!first)
				builder.append(',');

			builder.append('"');
			StringUtils.encodeHTML(entry.getKey(), builder);
			builder.append("\":");
			toJson(entry.getValue(), builder);
			first = false;
		}

		builder.append('}');
	}

	/**
	 * Uses reflection to serialize an unknown array into JSON representation
	 * 
	 * @param array
	 *            The array to be serialized
	 * @param builder
	 *            The string builder to append the array
	 */
	private static void serializeArray(Object array, StringBuilder builder) {
		builder.append('[');

		int length = Array.getLength(array);

		for (int i = 0; i < length; i++) {
			if (i > 0)
				builder.append(',');

			toJson(Array.get(array, i), builder);
		}

		builder.append(']');
	}

	/**
	 * Serialize the public attributes of an ordinary object
	 * 
	 * @param object
	 *            The object to be serialized
	 * @param builder
	 *            The builder to append the serialized data
	 */
	private static void ordnaryObject(Object object, StringBuilder builder) {
		Field[] fields = object.getClass().getDeclaredFields();

		boolean first = true;

		builder.append('{');
		for (Field field : fields) {
			if (!Modifier.isStatic(field.getModifiers())) {
				if (!first)
					builder.append(',');

				toJson(field.getName(), builder);
				builder.append(':');
				try {
					field.setAccessible(true);
					if (field.getType().isArray())
						serializeArray(field.get(object), builder);
					else
						toJson(field.get(object), builder);
				} catch (Exception e) {
					ExceptionUtils.throwWrapped(e);
				}
				first = false;
			}
		}
		builder.append('}');
	}

	/**
	 * Creates a JSON array representation of an array
	 * 
	 * @param array
	 *            An array of objects
	 * @return a JSON array String
	 */
	public static String toJson(Object[] array) {
		StringBuilder builder = new StringBuilder();
		toJson(array, builder);
		return builder.toString();
	}

	/**
	 * Appends a JSON array representation of an array
	 * 
	 * @param array
	 *            An array of objects
	 * @param builder
	 *            The build which the JSON string will be appended
	 */
	public static void toJson(Object[] array, StringBuilder builder) {

		boolean first = true;

		builder.append('[');

		for (Object object : array) {
			if (!first)
				builder.append(',');

			toJson(object, builder);

			first = false;
		}

		builder.append(']');
	}
}
